/*
Copyright 2007 Flaptor (flaptor.com) 

Licensed under the Apache License, Version 2.0 (the "License"); 
you may not use this file except in compliance with the License. 
You may obtain a copy of the License at 

    http://www.apache.org/licenses/LICENSE-2.0 

Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an "AS IS" BASIS, 
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
See the License for the specific language governing permissions and 
limitations under the License.
*/


package com.flaptor.hist4j;

import java.util.ArrayList;

/**
 * The HistogramDataNode stores the histogram data for a range of values.
 * It knows the minimum and maximum values for which it counts the number of instances.
 * When the count exceeds the allowed limit it splits itself in two, increasing the
 * histogram resolution for this range.
 * @author Jorge Handl
 */

public class HistogramDataNode extends HistogramNode {

	private static final long serialVersionUID = -1L;

   // Attributes of a data node.
	private Cell cell = new Cell();

    /**
     * Creates an empty data node.
     */
    public HistogramDataNode () {
        reset();
    }

    /**
     * Creates a data node for the given range with the given instance count.
     * @param count the number of data instances in the given range.
     * @param minValue the start of the range of counted values.
     * @param maxValue the end of the range of counted values.
     */
    public HistogramDataNode (long count, float minValue, float maxValue) {
        reset();
        cell.count = count;
        cell.minValue = minValue;
        cell.maxValue = maxValue;
    }

    /**
     * Clears the data node.
     */
    public void reset () {
        cell.count = 0;
        cell.minValue = Float.MAX_VALUE;
        cell.maxValue = -Float.MAX_VALUE;
    }

    /**
     * Adds a value to the data node.<p>
     * If the value falls inside of the nodes' range and the count does not exceed the imposed limit, it simply increments the count.<br>
     * If the value falls outside of the nodes' range, it expands the range.<br>
     * If the count exceeds the limit, it splits in two assuming uniform distribution inside the node.<br>
     * If the value falls outside of the nodes' range AND the count exceeds the limit, it creates a new node for that value.
     * @param root a reference to the adaptive histogram instance that uses this structure.
     * @param value the value for which the count is to be incremented.
     * @return A reference to itself if no structural change happened, or a reference to the new fork node if this node was split.
     */
    public HistogramNode addValue (AdaptiveHistogram root, float value) {
        // "self" is what is returned to the caller. If this node needs to be replaced by a fork node, 
        // this variable will hold the new fork node and it will be returned to the caller.
        // Otherwise, the node returned will be this, in which case nothing changes.
        HistogramNode self = this;
        if (value >= cell.minValue && value <= cell.maxValue) {  // the value falls within this nodes' range
            if (cell.count < root.getCountPerNodeLimit()  // there is enough room in this node for the new value
                    || cell.minValue == cell.maxValue) {  // or the node defines a zero-width range so it can't be split
            	cell.count++;
            } else {  // not enough room, distribute the value count among the new nodes, assuming uniform distribution
                float splitValue = (cell.minValue + cell.maxValue) / 2;
                long rightCount = cell.count / 2;
                long leftCount = rightCount;
                boolean countWasOdd = (leftCount + rightCount < cell.count);
                // assign the new value to the corresponding side. If the count is odd, add the extra item to the other side to keep balance
                if (value > splitValue) {
                    rightCount++;
                    leftCount += (countWasOdd?1:0);
                } else {
                    leftCount++;
                    rightCount += (countWasOdd?1:0);
                }
                // create a new subtree that will replace this node
                HistogramNode leftNode = new HistogramDataNode(leftCount, cell.minValue, splitValue);
                HistogramNode rightNode = new HistogramDataNode(rightCount, splitValue, cell.maxValue);
                self = new HistogramForkNode(splitValue, leftNode, rightNode);
            }
        } else {  // the value falls outside of this nodes' range
            if (cell.count < root.getCountPerNodeLimit()) {  // there is enough room in this node for the new value
            	cell.count++;
                // extend the range of this node, assuming that the tree structure above correctly directed 
                // the given value to this node and therefore it lies at one of the borders of the tree.
                if (value < cell.minValue) cell.minValue = value;
                if (value > cell.maxValue) cell.maxValue = value;
            } else {  // not enough room, create a new sibling node for the new value and put both under a new fork node
                if (value < cell.minValue) {
                	cell.minValue = Math.min(cell.minValue, (value + cell.maxValue) / 2);
                    self = new HistogramForkNode(cell.minValue, new HistogramDataNode(1,value,cell.minValue), this);
                } else {
                	cell.maxValue = Math.max(cell.maxValue, (cell.minValue + value) / 2);
                    self = new HistogramForkNode(cell.maxValue, this, new HistogramDataNode(1,cell.maxValue,value));
                }
            }
        }
        return self;
    }

    /**
     * Returns the number of data points stored in the same bucket as a given value.
     * @param value the reference data point.
     * @return the number of data points stored in the same bucket as the reference point.
     */
    public long getCount (float value) {
        long res = 0;
        if (value >= cell.minValue && value <= cell.maxValue) {
            res = cell.count;
        }
        return res;
    }

    /**
     * Returns the cumulative density function for a given data point.
     * @param value the reference data point.
     * @return the cumulative density function for the reference point.
     */
    public long getAccumCount (float value) {
        long res = 0;
        if (value >= cell.minValue) {
            res = cell.count;
        }
        return res;
    }

    // Linear interpolation for float values.
    private float interpolate (float x0, float y0, float x1, float y1, float x) {
        return y0+((x-x0)*(y1-y0))/(x1-x0);
    }


    /**
     * Returns the data point where the running cumulative count reaches the target cumulative count.
     * It uses linear interpolation over the range of the node to get a better estimate of the true value.
     * @param accumCount an array containing:<br>
     *      - accumCount[0] the running cumulative count. <br>
     *      - accumCount[1] the target cumulative count.
     * @return the data point where the running cumulative count reaches the target cumulative count.
     */
    public Float getValueForAccumCount (long[] accumCount) {
        Float res = null;
        long runningAccumCount = accumCount[0];
        long targetAccumCount = accumCount[1];
        if (runningAccumCount <= targetAccumCount && runningAccumCount + cell.count >= targetAccumCount) {
            float val = interpolate((float)runningAccumCount, cell.minValue, (float)(runningAccumCount + cell.count), cell.maxValue, (float)targetAccumCount); 
            res = new Float(val);
        }
        accumCount[0] += cell.count;
        return res;
    }

    /** 
     * Applies a convertion function to the values stored in the histogram.
     * @param valueConversion a class that defines a function to convert the value.
     */
    public void apply (AdaptiveHistogram.ValueConversion valueConversion) {
    	cell.minValue = valueConversion.convertValue(cell.minValue);
    	cell.maxValue = valueConversion.convertValue(cell.maxValue);
    }


    /**
     * Prints this nodes' data with a margin depending on the level of the node in the tree.
     * @param level the level of this node in the tree.
     */
    public void show (int level) {
        margin(level);
        System.out.println("Data: " + cell.count + " ("+cell.minValue+","+cell.maxValue+")");
    }
    
    /**
     * Build the table representing the histogram data adding this node's cell to it.
     */
    public void toTable (ArrayList<Cell> table) {
    	table.add(cell);
    }

}

